iT邦幫忙

2026 iThome 鐵人賽

DAY 6
0
Build on Google AI

30 天用 Google AI 打造台灣防災速報 App系列 第 6

Day 06|接上中央氣象署開放資料:授權碼沒錯,卻被憑證檢查擋在門外

  • 分享至 

  • xImage
  •  

系列:30 天用 Google AI 打造臺灣防災速報 App(Day 6/30)

NCDR 給了我們廣度,中央氣象署(CWA)開放資料平台給我們深度,尤其是地震報告的完整參數(規模、深度、各地震度),這是做地震速報摘要的關鍵原料。

申請授權碼

先澄清一個常見誤解:氣象署開放資料平台仍然開放、授權碼免費(寫作當下查證過;2023 年氣象局改制為氣象署時換過網域,可能因此有「不開放了」的印象)。

  1. 氣象資料開放平臺註冊會員。
  2. 登入後在「API授權碼」頁面取得授權碼。
  3. 同樣存成環境變數 CWA_API_KEY

本專案用到的資料集

資料集編號 內容 用途
E-A0015-001 顯著有感地震報告 地震速報摘要
E-A0016-001 小區域有感地震報告 補充小地震
W-C0033-001 天氣特報(各縣市) 豪雨、強風等特報
O-A0001-001 自動氣象站觀測 之後做在地化資訊

(實作時請在平台的資料列表逐一確認編號仍有效,資料集偶有調整。)

下載程式

import os
import ssl
import urllib.parse
import urllib.request
import json

BASE = "https://opendata.cwa.gov.tw/api/v1/rest/datastore"

# 實測發現:CWA 的政府憑證鏈缺 Subject Key Identifier 欄位(用來識別憑證所屬金鑰的欄位),
# Python 3.13 起預設開啟的 VERIFY_X509_STRICT(對憑證做額外嚴格檢查的設定)
# 會直接拒絕連線,
# 需在 context 關閉 strict 檢查(憑證本身仍有驗證)
ctx = ssl.create_default_context()
ctx.verify_flags &= ~ssl.VERIFY_X509_STRICT

def cwa_get(dataset: str, **params) -> dict:
    params["Authorization"] = os.environ["CWA_API_KEY"].strip()
    url = f"{BASE}/{dataset}?{urllib.parse.urlencode(params)}"
    with urllib.request.urlopen(url, timeout=30, context=ctx) as resp:
        return json.load(resp)

# 最近的顯著有感地震
quakes = cwa_get("E-A0015-001", limit=5)
for eq in quakes["records"]["Earthquake"]:
    info = eq["EarthquakeInfo"]
    print(info["OriginTime"], f"規模 {info['EarthquakeMagnitude']['MagnitudeValue']}",
          f"深度 {info['FocalDepth']} km", info["Epicenter"]["Location"])

地震報告的 JSON 結構層層巢狀(震央、規模、深度、各縣市震度分別在不同層),建議先把「震央位置、規模、深度、最大震度、發震時間」五個欄位整理成單層、容易取用的字典(flat dict),之後交給 Gemini 或存進 Firestore 都比較方便:

LEVELS = ["1級", "2級", "3級", "4級", "5弱", "5強", "6弱", "6強", "7級"]

def flatten(eq: dict) -> dict:
    info = eq["EarthquakeInfo"]
    areas = [a for a in eq["Intensity"]["ShakingArea"]
             if a["AreaIntensity"] in LEVELS]
    top = max((a["AreaIntensity"] for a in areas), key=LEVELS.index)
    counties = []
    for a in areas:
        if a["AreaIntensity"] == top:
            for name in a["CountyName"].split("、"):
                if name not in counties:
                    counties.append(name)
    return {
        "origin_time": info["OriginTime"],
        "location": info["Epicenter"]["Location"],
        "magnitude": info["EarthquakeMagnitude"]["MagnitudeValue"],
        "depth_km": info["FocalDepth"],
        "max_intensity": top,
        "max_intensity_counties": counties,
    }

震度不能直接當數字比大小,因為有「5弱」「5強」這種等級,所以先列出由小到大的順序(LEVELS),再找出最大的一級和對應的縣市。

實測結果(2026-09-17):上面的程式可直接執行,欄位名稱全部正確。最近五筆顯著有感地震的輸出如下:

2026-09-14T06:44:41+08:00 規模 4.9 深度 10.1 km 臺東縣政府東南東方 43.0 公里 (位於臺灣東南部海域)
2026-09-14T03:21:35+08:00 規模 4.7 深度 17.6 km 花蓮縣政府南南西方 54.9 公里 (位於花蓮縣萬榮鄉)
2026-09-11T19:24:56+08:00 規模 4.5 深度 24.6 km 花蓮縣政府南方 67.9 公里 (位於臺灣東部海域)
2026-09-07T00:37:01+08:00 規模 4.8 深度 32.1 km 臺東縣政府東南東方 23.8 公里 (位於臺灣東南部海域)
2026-09-04T17:11:00+08:00 規模 4.5 深度 13.7 km 臺東縣政府東南東方 44.9 公里 (位於臺灣東南部海域)

timeFromtimeTo 兩個參數也查得到歷史報告,例如 8 月 22 日那筆規模 5.1 的東部海域地震。報告最外層的 ReportContent 欄位就是官方的一句話摘要(「08/22-11:40臺灣東部海域發生規模5.1有感地震,最大震度宜蘭縣南澳、花蓮縣和平、宜蘭縣宜蘭市3級。」)。各縣市的最大震度則放在 Intensity 底下的 ShakingArea 陣列裡,逐一列出。官方雖然已經有一句話摘要,但之後要交給 Gemini 改寫和分級的是原始的數值,所以單層字典只保留參數。查詢這一筆並交給上面的 flatten()

aug = cwa_get("E-A0015-001", timeFrom="2026-08-22T00:00:00", timeTo="2026-08-23T00:00:00")
print(flatten(aug["records"]["Earthquake"][0]))

得到(9/19 執行):

{"origin_time": "2026-08-22T11:40:15+08:00", "location": "宜蘭縣政府東南方  41.7  公里 (位於臺灣東部海域)",
 "magnitude": 5.1, "depth_km": 27.8, "max_intensity": "3級", "max_intensity_counties": ["宜蘭縣", "花蓮縣"]}

W-C0033-001 同樣正常,9/17 查詢時苗栗、臺中、彰化、屏東、澎湖五縣市有陸上強風特報生效;沒有特報時該縣市會回傳空的 hazards 陣列,程式要處理這個情況。

唯一的問題是上面註解寫的 SSL 憑證檢查,查了一陣子才確認原因不在授權碼。

與 NCDR 的分工

  • NCDR:跨部會事件流,決定「現在有哪些警報」。
  • CWA:氣象與地震的完整參數,決定「這個警報的細節」。

兩者用「事件時間+地點」比對:NCDR 出現地震示警時,去 CWA 查詢完整地震報告補充細節。

今日小結

  • CWA 授權碼申請流程確認可行,四個目標資料集列表完成。
  • 地震報告下載程式完成,並把關鍵欄位整理成單層字典。

明天 Day 7 是第一次週回顧:把兩個資料源整合成一條資料流程,定稿整體設計。


上一篇
Day 05|1,074 筆示警、161 組重複 id:被真實資料推翻的去除重複設計
下一篇
Day 07|第一週回顧:為什麼資料的下載與整理不交給 AI
系列文
30 天用 Google AI 打造台灣防災速報 App9
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言